package com.tungse.minio.service;

import com.tungse.minio.dto.FileUploadInfo;
import com.tungse.minio.dto.UploadProgress;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.BitSet;

@Service
public class FileUploadService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private static final String UPLOAD_DIR = "D:/tmp/tomcat/uploads/";
    
    // 初始化文件上传信息
    public void initFileUpload(FileUploadInfo uploadInfo) {
        String redisKey = "file:" + uploadInfo.getFileMd5();
        if(Boolean.TRUE.equals(redisTemplate.hasKey(redisKey))) {
            throw new RuntimeException("文件以及初始化了， 请勿重复操作");
        }
        uploadInfo.setFilePath(UPLOAD_DIR + uploadInfo.getFileName());
        // 创建目录
        boolean b = new File(UPLOAD_DIR + uploadInfo.getFileMd5()).mkdirs();
        redisTemplate.opsForValue().set(redisKey, uploadInfo);
        //方案二 在初始时（initFileUpload）直接创建需要上传大小的文件， 分片上传时按照位置直接写入
        if(!uploadInfo.isScheme()) {
            //创建目标文件
            try(RandomAccessFile raf = new RandomAccessFile(uploadInfo.getFilePath(), "rw");) {
                raf.setLength(uploadInfo.getTotalSize());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

    }
    
    // 上传分片文件
    public boolean uploadChunk(String fileMd5, MultipartFile chunkFile, int chunkIndex) throws IOException {
        FileUploadInfo uploadInfo = (FileUploadInfo) redisTemplate.opsForValue().get("file:" + fileMd5);
        if (uploadInfo == null) {
            throw new RuntimeException("文件上传信息不存在");
        }
        // 上传分片时原子性设置位图
        Boolean previousValue = redisTemplate.opsForValue().setBit("file:chunks:" + fileMd5, chunkIndex, true);
        if (Boolean.TRUE.equals(previousValue)) {
            // 该分片已经上传过，避免重复操作
            throw new RuntimeException("该分片已经上传过，避免重复操作");
        }
        boolean scheme = uploadInfo.isScheme(); // true 测试方案一 false 测试方案二

        //方案一  上传的分片文件直接写入磁盘， 所有分片上传完成后， 统一合并分片文件
        if(scheme) {
            // 写入分片文件
            String chunkPath = UPLOAD_DIR + fileMd5 + "/" + chunkIndex;
            File chunk = new File(chunkPath);
            chunkFile.transferTo(chunk);
        } else {
            //方案二 在初始时（initFileUpload）直接创建需要上传大小的文件， 分片上传时按照位置直接写入
            // 计算分片在目标文件中的位置
            long position = (long) chunkIndex * uploadInfo.getChunkSize();

            // 直接写入目标文件的指定位置
            File targetFile = new File(uploadInfo.getFilePath());
            try (RandomAccessFile raf = new RandomAccessFile(targetFile, "rw")) {
                raf.seek(position);
                raf.write(chunkFile.getBytes());
            }
        }
        // 上传失败时将位图重置
        //redisTemplate.opsForValue().setBit("file:chunks:" + fileMd5, chunkIndex, false);
        // 检查是否全部上传完成
        Long uploadedCount = redisTemplate.execute((RedisCallback<Long>) connection ->
                connection.bitCount(("file:chunks:" + fileMd5).getBytes())
        );

        if (uploadedCount != null && uploadedCount == uploadInfo.getTotalChunks()) {
            // 所有分片已上传，执行合并操作
            if(scheme) {
                mergeChunks(uploadInfo); // 合并分片文件
            }
            redisTemplate.delete("file:" + fileMd5);
            redisTemplate.delete("file:chunks:" + fileMd5);
            return true;
        }
        return false;
    }

    // 所有分片文件上传完成后， 统一合并分片文件
    private void mergeChunks(FileUploadInfo uploadInfo) throws IOException {
        File targetFile = new File(uploadInfo.getFilePath());
        try (RandomAccessFile raf = new RandomAccessFile(targetFile, "rw")) {
            for (int i = 0; i < uploadInfo.getTotalChunks(); i++) {
                String chunkPath = UPLOAD_DIR + uploadInfo.getFileMd5() + "/" + i;
                File chunkFile = new File(chunkPath);
                
                try (FileInputStream fis = new FileInputStream(chunkFile)) {
                    byte[] buffer = new byte[4096];
                    int bytesRead;
                    while ((bytesRead = fis.read(buffer)) != -1) {
                        raf.write(buffer, 0, bytesRead);
                    }
                }
                
                // 删除分片文件
                chunkFile.delete();
            }
        }
        // 删除分片目录
        new File(UPLOAD_DIR + uploadInfo.getFileMd5()).delete();
    }
    
    // 获取上传进度
    public UploadProgress getUploadProgress(String fileMd5) {
        FileUploadInfo uploadInfo = (FileUploadInfo) redisTemplate.opsForValue().get("file:" + fileMd5);
//        FileUploadInfo uploadInfo = (FileUploadInfo) redisTemplate.opsForValue().get("file:" + fileMd5);
        if (uploadInfo == null) {
            return null;
        }
        
        UploadProgress progress = new UploadProgress();
        progress.setFileMd5(fileMd5);
        progress.setFileName(uploadInfo.getFileName());
        progress.setTotalChunks(uploadInfo.getTotalChunks());
//        progress.setUploadedChunks(uploadInfo.getUploadedChunks().cardinality());
//        progress.setCompleted(uploadInfo.isUploadComplete());
        
        return progress;
    }
}

